
#include "sbAlbumService.h"

#include <sbIAlbumViewScan.h>
#include <sbILibraryManager.h>
#include <sbILibrary.h>
#include <sbIJobProgress.h>
#include <sbIJobProgressService.h>
#include <sbIPropertyManager.h>
#include <sbPropertiesCID.h>
#include <sbStandardProperties.h>
#include <sbIPropertyArray.h>
#include <sbStringUtils.h>
#include <sbProxyUtils.h>

#include <nsComponentManagerUtils.h>
#include <nsThreadUtils.h>
#include <nsServiceManagerUtils.h>
#include <nsStringAPI.h>


// Interval for rescanning the main library
#define RESCAN_INTERVAL 10000


/**
 * To log this module, set the following environment variable:
 *   NSPR_LOG_MODULES=sbAlbumViews:5
 * Use the following to output to a file:
 *   NSPR_LOG_FILE=path/to/file.log
 */
#include "prlog.h"
#ifdef PR_LOGGING
static PRLogModuleInfo* gAlbumViewsLog = nsnull;
#define TRACE(args) PR_LOG(gAlbumViewsLog, PR_LOG_DEBUG, args)
#define LOG(args)   PR_LOG(gAlbumViewsLog, PR_LOG_WARN, args)
#else
#define TRACE(args) /* nothing */
#define LOG(args)   /* nothing */
#endif /* PR_LOGGING */

#ifndef kNotFound
#define kNotFound -1
#endif

//------------------------------------------------------------------------------
//
// nsISupports implementation.
//
//------------------------------------------------------------------------------

NS_IMPL_THREADSAFE_ADDREF(sbAlbumService)
NS_IMPL_THREADSAFE_RELEASE(sbAlbumService)

NS_INTERFACE_MAP_BEGIN(sbAlbumService)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, sbIAlbumService)
  NS_INTERFACE_MAP_ENTRY(sbIMediaListListener)
  NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_END_THREADSAFE

// I really don't know if the above is needed at all
// NS_IMPL_THREADSAFE_ISUPPORTS3(sbAlbumService,
//                               sbIAlbumService,
//                               sbIMediaListListener,
//                               nsIObserver)

//------------------------------------------------------------------------------
//
// sbIAlbumService implementation.
//
//------------------------------------------------------------------------------
NS_IMETHODIMP
sbAlbumService::ScanMainLibraryForAlbums(sbIAlbumViewScan* *_retVal)
{
  TRACE(("sbAlbumService[0x%.8x] - ScanMainLibraryForAlbums", this));
  nsresult rv;

  nsCOMPtr<sbILibraryManager> libManager =
      do_GetService("@songbirdnest.com/Songbird/library/Manager;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<sbILibrary> mLibrary;
  rv = libManager->GetMainLibrary(getter_AddRefs(mLibrary));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<sbIMediaList> mediaList = do_QueryInterface(mLibrary, &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  
  nsCOMPtr<sbIAlbumViewScan> albumViewScan =
    do_CreateInstance("@songbridnest.com/Songbird/albumviewscan;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  
  rv = albumViewScan->ScanListForAlbums(mediaList);
  NS_ENSURE_SUCCESS(rv, rv);
  
  NS_ADDREF(*_retVal = albumViewScan);
  return NS_OK;
}

NS_IMETHODIMP
sbAlbumService::ScanMainLibraryWithDialog()
{
  TRACE(("sbAlbumService[0x%.8x] - ScanMainLibraryWithDialog", this));
  nsresult rv;

  // This has to be running on the main thread because it creates a progress
  // dialog which is UI and UI is always on the main thread.
  if (!NS_IsMainThread()) {
    nsCOMPtr<sbIAlbumService> proxy;
    
    // This components implements from lots of sbI* interfaces, which all derive
    // from their own nsISupports. So we can't just pass |this| to
    // SB_GetProxyForObject because it won't know which of these nsISupports to
    // cast it to, so explictely cast it beforehand here.
    nsISupports* supports = nsnull;
    NS_ISUPPORTS_CAST(sbIAlbumService*, this);
    
    nsresult rv = SB_GetProxyForObject(NS_PROXY_TO_MAIN_THREAD,
                                       NS_GET_IID(sbIAlbumService),
                                       supports,
                                       nsIProxyObjectManager::INVOKE_SYNC,
                                       getter_AddRefs(proxy));
    NS_ENSURE_SUCCESS(rv, rv);
    rv = proxy->ScanMainLibraryWithDialog();
    return rv;
  }

  nsCOMPtr<sbIJobProgressService> jobProgressService =
    do_GetService("@songbirdnest.com/Songbird/JobProgressService;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<sbIAlbumViewScan> job;
  rv = ScanMainLibraryForAlbums(getter_AddRefs(job));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<sbIJobProgress> jobProgress = do_QueryInterface(job, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = jobProgressService->ShowProgressDialog(jobProgress,
                                              nsnull,
                                              nsnull); 
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

//------------------------------------------------------------------------------
//
// sbAlbumService implementation.
//
//------------------------------------------------------------------------------
sbAlbumService::sbAlbumService() :
  mInitialized(PR_FALSE),
  mPrefsAvailable(PR_FALSE),
  mNeedsRescan(PR_FALSE)
{
#ifdef PR_LOGGING
  if (!gAlbumViewsLog) {
    gAlbumViewsLog = PR_NewLogModule("sbAlbumViews");
  }
#endif
  TRACE(("sbAlbumService::Constructed [0x%.8x]", this));
}

sbAlbumService::~sbAlbumService()
{
  TRACE(("sbAlbumService::Destructed [0x%.8x]", this));
  Finalize();
}

nsresult
sbAlbumService::Initialize()
{
  TRACE(("sbAlbumService::Initialize [0x%.8x]", this));
  nsresult rv;

  // Do nothing if already initialized.
  if (mInitialized)
    return NS_OK;

  // Add observers.
  if (!mObserverService) {
    // Get the observer service.
    mObserverService = do_GetService("@mozilla.org/observer-service;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);

    // Add observers.
    rv = mObserverService->AddObserver(this,
                                       "profile-after-change",
                                       PR_FALSE);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = mObserverService->AddObserver(this,
                                       SB_LIBRARY_MANAGER_READY_TOPIC,
                                       PR_FALSE);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = mObserverService->AddObserver(this,
                                       SB_LIBRARY_MANAGER_BEFORE_SHUTDOWN_TOPIC,
                                       PR_FALSE);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Do nothing more if preferences are not available.
  if (!mPrefsAvailable)
    return NS_OK;

  // Add our albumGUID property if it does not exist
  nsCOMPtr<sbIPropertyManager> pMgr =
    do_GetService("@songbirdnest.com/Songbird/Properties/PropertyManager;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  PRBool hasProperty;
  rv = pMgr->HasProperty(NS_LITERAL_STRING(SB_ALBUM_PROP), &hasProperty);
  NS_ENSURE_SUCCESS(rv, rv);
  
  if (!hasProperty) {
    nsCOMPtr<sbITextPropertyInfo> albumGUIDProp =
      do_CreateInstance("@songbirdnest.com/Songbird/Properties/Info/Text;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = albumGUIDProp->SetId(NS_LITERAL_STRING(SB_ALBUM_PROP));
    NS_ENSURE_SUCCESS(rv, rv);
    rv = albumGUIDProp->SetDisplayName(NS_LITERAL_STRING("Album GUID"));
    NS_ENSURE_SUCCESS(rv, rv);
    rv = albumGUIDProp->SetUserEditable(PR_FALSE);
    NS_ENSURE_SUCCESS(rv, rv);
#ifdef DEBUG
    rv = albumGUIDProp->SetUserViewable(PR_TRUE);
#else
    rv = albumGUIDProp->SetUserViewable(PR_FALSE);
#endif
    NS_ENSURE_SUCCESS(rv, rv);
    
    rv = pMgr->AddPropertyInfo(albumGUIDProp);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Create our timer to rescan the main library
  mRescanTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // Mark component as initialized.
  mInitialized = PR_TRUE;

  return NS_OK;
}

//------------------------------------------------------------------------------
//
// sbIMediaListListener implementation.
//
//------------------------------------------------------------------------------

NS_IMETHODIMP
sbAlbumService::OnItemAdded(sbIMediaList *aMediaList,
                            sbIMediaItem *aMediaItem,
                            PRUint32 aIndex,
                            PRBool *_retval)
{
  NS_ENSURE_ARG_POINTER(aMediaList);
  NS_ENSURE_ARG_POINTER(aMediaItem);
  NS_ENSURE_ARG_POINTER(_retval);
  TRACE(("sbAlbumService[0x%.8x] - OnItemAdded", this));

  // At least one item has been added, mark the component to perform a rescan
  mNeedsRescan = PR_TRUE;

  // Rescan now if not in a batch
  if (!mBatchHelper.IsActive()) {
    // Start the timer to rescan the library
    nsresult rv;
    rv = mRescanTimer->Init(this, 
                            RESCAN_INTERVAL,
                            nsITimer::TYPE_ONE_SHOT);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Prevent further notifications of this if in a batch
  *_retval = PR_TRUE;
  return NS_OK;
}
 
NS_IMETHODIMP
sbAlbumService::OnBeforeItemRemoved(sbIMediaList *aMediaList,
                                    sbIMediaItem *aMediaItem,
                                    PRUint32 aIndex,
                                    PRBool *_retval)
{
  NS_ENSURE_ARG_POINTER(aMediaList);
  NS_ENSURE_ARG_POINTER(aMediaItem);
  NS_ENSURE_ARG_POINTER(_retval);
  TRACE(("sbAlbumService[0x%.8x] - OnBeforeItemRemoved", this));

  // Prevent further notifications of this if in a batch
  *_retval = PR_TRUE;
  return NS_OK;
}

NS_IMETHODIMP
sbAlbumService::OnAfterItemRemoved(sbIMediaList *aMediaList,
                                   sbIMediaItem *aMediaItem,
                                   PRUint32 aIndex,
                                   PRBool *_retval)
{
  NS_ENSURE_ARG_POINTER(aMediaList);
  NS_ENSURE_ARG_POINTER(aMediaItem);
  NS_ENSURE_ARG_POINTER(_retval);
  TRACE(("sbAlbumService[0x%.8x] - OnAfterItemRemoved", this));

  // Prevent further notifications of this if in a batch
  *_retval = PR_TRUE;
  return NS_OK;
}

NS_IMETHODIMP
sbAlbumService::OnItemUpdated(sbIMediaList *aMediaList,
                              sbIMediaItem *aMediaItem,
                              sbIPropertyArray *aProperties,
                              PRBool *_retval)
{
  NS_ENSURE_ARG_POINTER(aMediaList);
  NS_ENSURE_ARG_POINTER(aMediaItem);
  NS_ENSURE_ARG_POINTER(aProperties);
  NS_ENSURE_ARG_POINTER(_retval);
  TRACE(("sbAlbumService[0x%.8x] - OnItemUpdated", this));

  // An item has been updated, mark the component to perform a rescan
  mNeedsRescan = PR_TRUE;

  // Reset the albumGUID property on the item so it gets scanned
  nsresult rv;
  rv = aMediaItem->SetProperty(NS_LITERAL_STRING(SB_ALBUM_PROP),
                               EmptyString());

  // Rescan now if not in a batch
  if (!mBatchHelper.IsActive()) {
    // Start the timer to rescan the library
    rv = mRescanTimer->Init(this, 
                            RESCAN_INTERVAL,
                            nsITimer::TYPE_ONE_SHOT);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // We want to be notified of each item
  *_retval = PR_FALSE;
  return NS_OK;
}

NS_IMETHODIMP
sbAlbumService::OnItemMoved(sbIMediaList *aMediaList,
                            PRUint32 aFromIndex,
                            PRUint32 aToIndex,
                            PRBool *_retval)
{
  NS_ENSURE_ARG_POINTER(aMediaList);
  NS_ENSURE_ARG_POINTER(_retval);
  TRACE(("sbAlbumService[0x%.8x] - OnItemMoved", this));

  // Prevent further notifications of this if in a batch
  *_retval = PR_TRUE;
  return NS_OK;
}

NS_IMETHODIMP
sbAlbumService::OnListCleared(sbIMediaList *aMediaList,
                              PRBool *_retval)
{
  NS_ENSURE_ARG_POINTER(aMediaList);
  NS_ENSURE_ARG_POINTER(_retval);
  TRACE(("sbAlbumService[0x%.8x] - OnListCleared", this));

  // Prevent further notifications of this if in a batch
  *_retval = PR_TRUE;
  return NS_OK;
}

NS_IMETHODIMP
sbAlbumService::OnBatchBegin(sbIMediaList *aMediaList)
{
  NS_ENSURE_ARG_POINTER(aMediaList);
  TRACE(("sbAlbumService[0x%.8x] - OnBatchBegin", this));

  // Increment our counter so we know we are in a batch operation
  mBatchHelper.Begin();
  return NS_OK;
}
 
NS_IMETHODIMP
sbAlbumService::OnBatchEnd(sbIMediaList *aMediaList)
{
  NS_ENSURE_ARG_POINTER(aMediaList);
  
  // Decrement our counter so we can tell when we are no longer in a batch
  // operation
  mBatchHelper.End();
  TRACE(("sbAlbumService[0x%.8x] - OnBatchEnd %d",
         this,
         mBatchHelper.Depth()));

  if (!mBatchHelper.IsActive() && mNeedsRescan) {
    mNeedsRescan = PR_FALSE;
    // Start the timer to rescan the library
    nsresult rv;
    rv = mRescanTimer->Init(this, 
                            RESCAN_INTERVAL,
                            nsITimer::TYPE_ONE_SHOT);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  return NS_OK;
}

//------------------------------------------------------------------------------
//
// nsIObserver implementation.
//
//------------------------------------------------------------------------------

NS_IMETHODIMP
sbAlbumService::Observe(nsISupports*     aSubject,
                        const char*      aTopic,
                        const PRUnichar* aData)
{
  TRACE(("sbAlbumService[0x%.8x] - Observe", this));
  nsresult rv;

  // Dispatch processing of event.
  if (!strcmp(aTopic, "profile-after-change")) {
    // Mark preferences as available and continue with initialization.
    mPrefsAvailable = PR_TRUE;
    rv = Initialize();
    NS_ENSURE_SUCCESS(rv, rv);
  } else if (!strcmp(aTopic, SB_LIBRARY_MANAGER_READY_TOPIC)) {
    // Initialize library specific stuff.
    rv = InitializeLibraryWatch();
    NS_ENSURE_SUCCESS(rv, rv);
  } else if (!strcmp(aTopic, SB_LIBRARY_MANAGER_BEFORE_SHUTDOWN_TOPIC)) {
    // Finalize the album service.
    Finalize();
  } else if (!strcmp(NS_TIMER_CALLBACK_TOPIC, aTopic)) {
    nsCOMPtr<nsITimer> aObserveTimer = do_QueryInterface(aSubject, &rv);
    NS_ENSURE_SUCCESS(rv, rv);
    if (aObserveTimer == mRescanTimer) {
      rv = ScanMainLibraryWithDialog();
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  return NS_OK;
}

//------------------------------------------------------------------------------
//
// Internal services.
//
//------------------------------------------------------------------------------

/**
 * Add a listener to the library.
 */

nsresult
sbAlbumService::InitializeLibraryWatch()
{
  TRACE(("sbAlbumService[0x%.8x] - InitializeLibraryWatch", this));
  nsresult rv;

  // Get the main library so we can listen to changes
  nsCOMPtr<sbILibrary> mainLibrary;
  rv = GetMainLibrary(getter_AddRefs(mainLibrary));
  NS_ENSURE_SUCCESS(rv, rv);

  mMainLibraryList = do_QueryInterface(mainLibrary, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // Properties that are relevant to the album service
  nsCOMPtr<sbIMutablePropertyArray> propFilter =
    do_CreateInstance(SB_MUTABLEPROPERTYARRAY_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // Don't validate since we are not putting values
  rv = propFilter->SetStrict(PR_FALSE);
  NS_ENSURE_SUCCESS(rv, rv);

  // AlbumName, ArtistName, AlbumArtistName, DiscNumber
  rv = propFilter->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_ALBUMNAME),
                                  EmptyString());
  NS_ENSURE_SUCCESS(rv, rv);
  rv = propFilter->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_ARTISTNAME),
                                  EmptyString());
  NS_ENSURE_SUCCESS(rv, rv);
  rv = propFilter->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_ALBUMARTISTNAME),
                                  EmptyString());
  NS_ENSURE_SUCCESS(rv, rv);
  rv = propFilter->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_DISCNUMBER),
                                  EmptyString());
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<sbIPropertyArray> propArray = do_QueryInterface(propFilter, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = mMainLibraryList->AddListener(this,
                                     PR_FALSE,  /* not weak */
                                     sbIMediaList::LISTENER_FLAGS_ITEMADDED |
                                       sbIMediaList::LISTENER_FLAGS_ITEMUPDATED |
                                       sbIMediaList::LISTENER_FLAGS_BATCHBEGIN |
                                       sbIMediaList::LISTENER_FLAGS_BATCHEND,
                                     propArray);
  NS_ENSURE_SUCCESS(rv, rv);
  
  return NS_OK;
}

/**
 * Finalize the album art service.
 */

void
sbAlbumService::Finalize()
{
  TRACE(("sbAlbumService[0x%.8x] - Finalize", this));
  nsresult rv;

  // Remove observers.
  if (mObserverService) {
    mObserverService->RemoveObserver(this,
                                     "profile-after-change");
    mObserverService->RemoveObserver(this,
                                     SB_LIBRARY_MANAGER_READY_TOPIC);
    mObserverService->RemoveObserver(this,
                                     SB_LIBRARY_MANAGER_BEFORE_SHUTDOWN_TOPIC);
    mObserverService = nsnull;
  }

  if (mRescanTimer) {
    rv = mRescanTimer->Cancel();
    NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to cancel rescan timer");
    mRescanTimer = nsnull;
  }

  if (mMainLibraryList) {
    rv = mMainLibraryList->RemoveListener(this);
    NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to remove main library listener");
  }
}

